/*******************************************************************************
 *
 * DSM - Distributed Search Manager
 * Developed by Milspec Research International Pty Ltd
 *
 * $Author: skahl $
 * $Revision: 1.17 $
 * $Date: 2007/12/05 22:28:06 $
 *
 * (c)Copyright 2004
 *    education.au limited
 *    DEST
 *    All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the names education.au limited, DEST nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * To the extent permitted by law, the copyright owners of this software and
 * its contributors:
 *
 *  (i) exclude all warranties in relation to the software; and
 * (ii) exclude liability for all losses, costs, expenses and damages arising
 *      in any way from the use of the software whether arising from or in
 *      relation to breach of contract, negligence or any other tort, in equity
 *      or otherwise. If the software is in breach of a warranty which is
 *      implied by law, the copyright owners and contributors liability is
 *      limited to the replacement of the software.
 *
 *******************************************************************************
 */

package au.edu.educationau.opensource.dsm.servlet;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.apache.log4j.Logger;
import org.xml.sax.helpers.AttributesImpl;

import au.edu.educationau.opensource.dsm.DeusExMachina;
import au.edu.educationau.opensource.dsm.adapters.SearchAdapterProperties;
import au.edu.educationau.opensource.dsm.obj.ResultCategory;
import au.edu.educationau.opensource.dsm.obj.ResultMetadata;
import au.edu.educationau.opensource.dsm.obj.SRWRequestHandler;
import au.edu.educationau.opensource.dsm.obj.SearchCriteria;
import au.edu.educationau.opensource.dsm.obj.SearchResult;
import au.edu.educationau.opensource.dsm.obj.SearchResultSet;
import au.edu.educationau.opensource.dsm.service.PropertiesService;
import au.edu.educationau.opensource.dsm.util.EducationAuUtils;
import au.edu.educationau.opensource.dsm.util.StringUtils;
import au.edu.educationau.opensource.dsm.worker.ResultSetUnifier;
import au.edu.educationau.opensource.dsm.worker.SearchAdapterFactory;
import au.edu.educationau.opensource.dsm.worker.SearchJob;
import au.edu.educationau.opensource.dsm.worker.SearchResultSorter;

/**
 * This SOAP SRW Serlvet is designed to generate the envelope and body for the
 * result set generated from searches. In this configuration, it is quite simple
 * and does not require the full blown Apache SOAP implementation. However,
 * having said that, it would be a good idea to integrate Axis into this, to
 * ease future web services access. - Gus 28/02/04
 * 
 * This is now being used for SRW - Gus 01/10/05
 * 
 * 
 */

public class DSMAPISoap extends HttpServlet {
	static Logger logger = Logger.getLogger(DSMAPISoap.class.getName());

	private static final float SRW_MAX_VERSION = 1.1f;
	private static final String SRW_RECORD_PACKING = "XML";
	private static final String SRW_DC_RECORD_SCHEMA = "info:srw/schema/1/dc-v1.1";
	private static final String XMLNS_SRW = "http://www.loc.gov/zing/srw/";
	private static final String XMLNS_DIAG = "http://www.loc.gov/zing/srw/diagnostic/";
	private static final String XMLNS_XCQL = "http://www.loc.gov/zing/cql/xcql/";
	private static final String XMLNS_DSM = "http://www.edna.edu.au/schema/edna_dsm_v1p0/";
	private static final String XMLNS_ZR = "http://explain.x3950.org/dtd/2.0/";
	private static final String XMLNS_SOAP = "http://schemas.xmlsoap.org/soap/envelope/";
	private static final String XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance";

	/**
	 * If you change XMLNS_SRW_DC, make sure you update XSI_DC_SCHEMALOCATION's
	 * first part to match.
	 */
	private static final String XMLNS_DC = "http://purl.org/dc/elements/1.1/";
	/**
	 * If you change XMLNS_SRW_DC, make sure you update XSI_DC_SCHEMALOCATION's
	 * first part to match.
	 */
	private static final String XMLNS_SRW_DC = "info:srw/schema/1/dc-schema";
	private static final String XSI_DC_SCHEMALOCATION = "info:srw/schema/1/dc-schema http://www.loc.gov/z3950/agency/zing/srw/dc-schema.xsd";

	private static final String DATABASE_INFO = "DSM - ESFS Collections";

	/** Servlet Context */
	protected ServletContext servletContext;

	/**
	 * Servlet init
	 * 
	 * @param config
	 * @exception ServletException
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		this.servletContext = config.getServletContext();
	}

	/**
	 * GET is blocked for SOAP
	 * 
	 * @param request
	 * @param response
	 * @exception ServletException
	 * @exception IOException
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		response.setContentType("text/xml");
		PrintWriter out = response.getWriter();
		showDiagnostics(0, out, "4", "", "Unsupported Operation - HTTP 1.1/GET not supported for SRW mode");
	}

	/**
	 * POST is used to handle all SOAP requests
	 * 
	 * @param request
	 * @param response
	 * @exception ServletException
	 * @exception IOException
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		response.setContentType("text/xml; charset=UTF-8");
		SearchCriteria criteria = new SearchCriteria();
		SearchResultSet set = null;
		SearchResult[] results = new SearchResult[0];
		String rset = "";
		int count = 0;
		String found = "0";
		int total = 0;
		int nextRecordPos = -1;
		String theoreticalTotal = "0";
		String timeTaken = "0.00";
		String allowDuplicates = "true";
		boolean isCompletedSet = true;
		int start = 1;
		int pageSize = 10;
		long token = 0;

		int operation = 0;

		/** Get the servlet input */
		ServletInputStream sInputStream = request.getInputStream();
		BufferedReader rdr = new BufferedReader(new InputStreamReader(sInputStream));

		/** Logging */
		StringBuffer soapReq = new StringBuffer();
		String line;
		while ((line = rdr.readLine()) != null) {
			soapReq.append(line);
		}

		StringReader reader = new StringReader(soapReq.toString());

		SRWRequestHandler srwHandler = new SRWRequestHandler();
		srwHandler = (SRWRequestHandler) ResultSetUnifier.processXML(reader, srwHandler);
		String user = srwHandler.getUser();

		// if (DeusExMachina._UserManagerService().authenticatedUser(user)) {
		criteria = srwHandler.getSearchCriteria();
		start = srwHandler.getStart();
		allowDuplicates = srwHandler.getAllowDuplicates();
		pageSize = srwHandler.getPageSize();
		rset = srwHandler.getRset();
		operation = srwHandler.getOperation();

		String[] sources = criteria.getSources();

		if (sources.length > 0) {
			sources = EducationAuUtils.removeDuplicates(sources);
			criteria.setSources(sources);
		}

		if (operation == SRWRequestHandler.OPERATION_EXPLAIN || (sources.length > 0 && sources[0].equals("all"))) {
			criteria.setQuery("");
			criteria.setSources(SearchAdapterFactory.getSources());
		}

		token = srwHandler.getToken();

		if (token <= 0) {
			token = DeusExMachina.generateToken();
		}

		criteria.setToken(token);

		if (logger.isInfoEnabled()) {
			logger.info("REQUEST " + user + " " + token + " SRW " + (operation == SRWRequestHandler.OPERATION_EXPLAIN ? "explain" : "searchRetrieve") + " " + soapReq);
		}

		/** check if the job exists */
		if (DeusExMachina._SearchManagerService().jobExists(token)) {
			SearchJob job = DeusExMachina._SearchManagerService().getJob(token);
			criteria = job.getSearchCriteria();
			DeusExMachina._SearchManagerService().monitorJob(token);
		} else {
			if (operation == SRWRequestHandler.OPERATION_SEARCHRETRIEVE && criteria.getQuery().length() <= 0) {
				showDiagnostics(token, response.getWriter(), "7", "query", "Mandatory Parameter not supplied - Please supply a query string");
				return;
			}

			if (operation == SRWRequestHandler.OPERATION_SEARCHRETRIEVE && srwHandler.getVersion().length() <= 0) {
				showDiagnostics(token, response.getWriter(), "7", "version", "Mandatory Parameter not supplied - Please supply a version");
				return;
			}

			float requestedVersion = 0f;
			try {
				requestedVersion = Float.parseFloat(srwHandler.getVersion());
			} catch (NumberFormatException nfe) {
			}

			if (operation == SRWRequestHandler.OPERATION_SEARCHRETRIEVE && requestedVersion > SRW_MAX_VERSION) {
				showDiagnostics(token, response.getWriter(), "5", "" + SRW_MAX_VERSION, "Unsupported version");
				return;
			}

			/** Check the criteria for CQL */
			if (operation == SRWRequestHandler.OPERATION_SEARCHRETRIEVE && criteria.getQuery().length() > 0) {
				org.z3950.zing.cql.CQLParser cqlParser = new org.z3950.zing.cql.CQLParser();
				String cqlTree = null;
				try {
					// Clean-up the CQL query first - as it might be encoded to
					// UTF-8
					// Decodes a application/x-www-form-urlencoded string using
					// a specific encoding scheme
					cqlTree = cqlParser.parse(java.net.URLDecoder.decode(criteria.getQuery(), "UTF-8")).toXCQL(0);
				} catch (IOException ioe) {
					showDiagnostics(token, response.getWriter(), "10", criteria.getQuery(), "Query Syntax Error - I/O Exception");
					return;
				} catch (org.z3950.zing.cql.CQLParseException cpe) {
					showDiagnostics(token, response.getWriter(), "10", criteria.getQuery(), "Query Syntax Error: " + cpe.getMessage());
					return;
				}

				/** Rebuild criteria using this cql Tree */
				logger.debug(cqlTree);
				criteria.setCQLTree(cqlTree);
				String cqlQuery = criteria.getQuery();
				cqlQuery = cqlQuery.replaceAll("\"", "%22");
				cqlQuery = cqlQuery.replaceAll(" ", "%20");
				criteria.setCQLQuery(criteria.getQuery());
				criteria = DeusExMachina._SearchManagerService().processCQLQuery(criteria);
			}

			/** generate a new search */
			DeusExMachina._SearchManagerService().search(criteria);
		}
		DeusExMachina.logSearchToDatabase(DeusExMachina._SearchManagerService().getJob(token), request);

		/** Priority Sorting - Added 25/5/04 by gsingh */
		sources = criteria.getSources();
		String[] prioritisedSources = new String[sources.length];
		String priority;
		String sortAlgorithm = StringUtils.checkString(srwHandler.getSortKeys(), SearchResultSorter.SORTED_BY_PRIORITY_INTERLEAVED);

		for (int i = 0; i < sources.length; i++) {
			priority = StringUtils.checkString(srwHandler.getPriority("priority." + sources[i]), "");
			if (priority.length() <= 0) {
				if (SearchAdapterFactory.adapterExists(sources[i])) {
					priority = "" + SearchAdapterFactory.getAdapterProperties(sources[i]).getPriority();
				} else {
					priority = "1000";
				}
			}
			prioritisedSources[i] = priority + "." + sources[i];
		}

		Arrays.sort(prioritisedSources);
		/** Priority Sorting - Added 25/5/04 by gsingh */

		set = DeusExMachina._SearchResultService().getResults(token);

		if (null != set) {
			isCompletedSet = set.isCompletedSet();
			total = set.getRawResultCount();
			if (rset.length() <= 0) {
				theoreticalTotal = "" + set.getTotalTheoreticalResultsAvailable();
			} else {
				theoreticalTotal = "" + set.getTheoreticalResultsAvailable(rset);
			}
			if (rset.equals("")) {
				results = set.getDistributedResults(start, pageSize, criteria.getMaxResults(), sortAlgorithm, allowDuplicates.equals("true"), prioritisedSources);
				timeTaken = "" + set.getTotalTimeTaken() / 1000f;
			} else {
				results = set.getResultsbySource(start, pageSize, rset, sortAlgorithm);
				timeTaken = "" + set.getTimeTakenBySource(rset) / 1000f;
			}
			found = "" + set.getSetResultCount();
			if (null != results) {
				count = results.length;
			}
		}

		/** Build output */
		logger.info("RESPONSE " + token + " " + found + " " + timeTaken + " 200");

		PrintWriter out = response.getWriter();
		try {
			StreamResult streamResult = new StreamResult(out);
			SAXTransformerFactory tf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
			// SAX2.0 ContentHandler.
			TransformerHandler hd = tf.newTransformerHandler();
			Transformer serializer = hd.getTransformer();
			serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
			serializer.setOutputProperty(OutputKeys.INDENT, "yes");
			hd.setResult(streamResult);
			hd.startDocument();
			AttributesImpl atts = new AttributesImpl();

			// SOAP Envelope start
			atts.addAttribute(XMLNS_SOAP, "soap", "xmlns:soap", "string", XMLNS_SOAP);
			hd.startElement("", "Envelope", "soap:Envelope", atts);
			atts.clear();
			hd.startElement("", "Body", "soap:Body", atts);

			if (operation == SRWRequestHandler.OPERATION_EXPLAIN) {
				// START <srw:explainResponse>
				atts.addAttribute(XMLNS_SRW, "srw", "xmlns:srw", "string", XMLNS_SRW);
				atts.addAttribute(XMLNS_ZR, "zr", "xmlns:zr", "string", XMLNS_ZR);
				atts.addAttribute(XMLNS_DSM, "dsm", "xmlns:dsm", "string", XMLNS_DSM);
				hd.startElement("", "explainResponse", "srw:explainResponse", atts);
				atts.clear();
			} else {
				// START <srw:searchRetrieveResponse>
				atts.addAttribute(XMLNS_SRW, "srw", "xmlns:srw", "string", XMLNS_SRW);
				atts.addAttribute(XMLNS_DIAG, "diag", "xmlns:diag", "string", XMLNS_DIAG);
				atts.addAttribute(XMLNS_XCQL, "xcql", "xmlns:xcql", "string", XMLNS_XCQL);
				atts.addAttribute(XMLNS_DSM, "dsm", "xmlns:dsm", "string", XMLNS_DSM);
				atts.addAttribute(XMLNS_DC, "dc", "xmlns:dc", "string", XMLNS_DC);
				atts.addAttribute(XMLNS_SRW_DC, "srw_dc", "xmlns:srw_dc", "string", XMLNS_SRW_DC);
				atts.addAttribute(XMLNS_XSI, "xsi", "xmlns:xsi", "string", XMLNS_XSI);
				hd.startElement("", "searchRetrieveResponse", "srw:searchRetrieveResponse", atts);
				atts.clear();
			}

			hd.startElement("", "version", "srw:version", atts);
			hd.characters(("" + SRW_MAX_VERSION).toCharArray(), 0, ("" + SRW_MAX_VERSION).length());
			hd.endElement("", "version", "srw:version");

			if (operation == SRWRequestHandler.OPERATION_SEARCHRETRIEVE) {
				hd.startElement("", "numberOfRecords", "srw:numberOfRecords", atts);
				hd.characters(("" + total).toCharArray(), 0, ("" + total).length());
				hd.endElement("", "numberOfRecords", "srw:numberOfRecords");

				hd.startElement("", "resultSetId", "srw:resultSetId", atts);
				hd.characters(("" + token).toCharArray(), 0, ("" + token).length());
				hd.endElement("", "resultSetId", "srw:resultSetId");

			} else if (operation == SRWRequestHandler.OPERATION_EXPLAIN) {
				hd.startElement("", "record", "srw:record", atts);
				hd.startElement("", "recordSchema", "srw:recordSchema", atts);
				hd.characters(XMLNS_ZR.toCharArray(), 0, XMLNS_ZR.length());
				hd.endElement("", "recordSchema", "srw:recordSchema");
				hd.startElement("", "recordData", "srw:recordData", atts);
				hd.startElement("", "explain", "zr:explain", atts);
				hd.startElement("", "serverInfo", "zr:serverInfo", atts);
				hd.startElement("", "host", "host", atts);
				hd.characters(request.getServerName().toCharArray(), 0, request.getServerName().length());
				hd.endElement("", "host", "host");
				hd.startElement("", "port", "port", atts);
				hd.characters(("" + request.getServerPort()).toCharArray(), 0, ("" + request.getServerPort()).length());
				hd.endElement("", "port", "port");
				hd.startElement("", "database", "database", atts);
				hd.characters(("esfs").toCharArray(), 0, ("esfs").length());
				hd.endElement("", "database", "database");
				hd.endElement("", "serverInfo", "zr:serverInfo");
				hd.startElement("", "databaseInfo", "zr:databaseInfo", atts);
				hd.startElement("", "title", "title", atts);
				hd.characters(DATABASE_INFO.toCharArray(), 0, DATABASE_INFO.length());
				hd.endElement("", "title", "title");
				hd.startElement("", "description", "description", atts);
				hd.characters(DATABASE_INFO.toCharArray(), 0, DATABASE_INFO.length());
				hd.endElement("", "description", "description");
				hd.endElement("", "databaseInfo", "zr:databaseInfo");
				hd.startElement("", "metaInfo", "zr:metaInfo", atts);
				hd.endElement("", "metaInfo", "zr:metaInfo");
				hd.endElement("", "explain", "zr:explain");
				hd.endElement("", "recordData", "srw:recordData");
				hd.endElement("", "record", "srw:record");
			}

			if (null != results && results.length > 0) {
				// Result Items <srw:records>
				hd.startElement("", "records", "srw:records", atts);

				SearchResult result;
				ResultCategory[] categories;
				ResultMetadata[] metadata;
				ResultCategory category;
				ResultMetadata metadataEntry;

				for (int i = 0; i < results.length; i++) {
					atts.clear();
					result = results[i];
					if (null != result) {
						String categoryURI = SearchAdapterFactory.getCategoryURI(result.getSource());
						// <srw:record>
						hd.startElement("", "record", "srw:record", atts);
						// Basic elements
						hd.startElement("", "recordPacking", "srw:recordPacking", atts);
						hd.characters(SRW_RECORD_PACKING.toCharArray(), 0, SRW_RECORD_PACKING.length());
						hd.endElement("", "recordPacking", "srw:recordPacking");

						hd.startElement("", "recordSchema", "srw:recordSchema", atts);
						hd.characters(SRW_DC_RECORD_SCHEMA.toCharArray(), 0, SRW_DC_RECORD_SCHEMA.length());
						hd.endElement("", "recordSchema", "srw:recordSchema");

						// <srw:recordData>
						hd.startElement("", "recordData", "srw:recordData", atts);
						// <srw_dc:dc>
						atts.addAttribute(XSI_DC_SCHEMALOCATION, "schemaLocation", "xsi:schemaLocation", "string", XSI_DC_SCHEMALOCATION);
						hd.startElement("", "dc", "srw_dc:dc", atts);
						atts.clear();
						// Title
						hd.startElement("", "title", "dc:title", atts);
						hd.characters(result.getTitle().toCharArray(), 0, result.getTitle().length());
						hd.endElement("", "title", "dc:title");
						// Description
						hd.startElement("", "description", "dc:description", atts);
						hd.characters(result.getDescription().toCharArray(), 0, result.getDescription().length());
						hd.endElement("", "description", "dc:description");
						// Identifier
						hd.startElement("", "identifier", "dc:identifier", atts);
						hd.characters(result.getLink().toCharArray(), 0, result.getLink().length());
						hd.endElement("", "identifier", "dc:identifier");

						if (result.getDate() != null && result.getDate().length() > 0) {
							hd.startElement("", "date", "dc:date", atts);
							hd.characters(result.getDate().toCharArray(), 0, result.getDate().length());
							hd.endElement("", "date", "dc:date");
						}
						// </srw_dc:dc>
						hd.endElement("", "dc", "srw_dc:dc");
						// </srw:recordData>
						hd.endElement("", "recordData", "srw:recordData");

						// <srw:extraRecordData>
						hd.startElement("", "extraRecordData", "srw:extraRecordData", atts);
						hd.startElement("", "sourceid", "dsm:sourceid", atts);
						hd.characters(result.getSource().toCharArray(), 0, result.getSource().length());
						hd.endElement("", "sourceid", "dsm:sourceid");

						hd.startElement("", "relevance", "dsm:relevance", atts);
						hd.characters(("" + result.getRelevance()).toCharArray(), 0, ("" + result.getRelevance()).length());
						hd.endElement("", "relevance", "dsm:relevance");

						hd.startElement("", "download", "dsm:download", atts);
						hd.characters(result.getDownload().toCharArray(), 0, result.getDownload().length());
						hd.endElement("", "download", "dsm:download");

						hd.startElement("", "preview", "dsm:preview", atts);
						hd.characters(result.getPreview().toCharArray(), 0, result.getPreview().length());
						hd.endElement("", "preview", "dsm:preview");

						hd.startElement("", "rights", "dsm:rights", atts);
						hd.characters(result.getRights().toCharArray(), 0, result.getRights().length());
						hd.endElement("", "rights", "dsm:rights");

						hd.startElement("", "metadatascheme", "dsm:metadatascheme", atts);
						hd.characters(result.getMetadataScheme().toCharArray(), 0, result.getMetadataScheme().length());
						hd.endElement("", "metadatascheme", "dsm:metadatascheme");

						hd.startElement("", "contentpackage", "dsm:contentpackage", atts);
						hd.characters(result.getContentPackage().toCharArray(), 0, result.getContentPackage().length());
						hd.endElement("", "contentpackage", "dsm:contentpackage");

						// Category Data
						categories = result.getResultCategories();
						if (null != categories) {
							hd.startElement("", "categories", "dsm:categories", atts);
							for (int j = 0; j < categories.length; j++) {
								category = categories[j];
								if (null != category) {
									hd.startElement("", "category", "dsm:category", atts);
									hd.startElement("", "title", "dsm:title", atts);
									hd.characters(category.getName().toCharArray(), 0, category.getName().length());
									hd.endElement("", "title", "dsm:title");
									for (Iterator it = category.getPaths().iterator(); it.hasNext();) {
										hd.startElement("", "", "link", atts);
										String path = it.next().toString();
										hd.characters((categoryURI + path).toCharArray(), 0, (categoryURI + path).length());
										hd.endElement("", "", "link");
									}
									hd.endElement("", "category", "dsm:category");
								} // if
							} // for
							hd.endElement("", "categories", "dsm:categories");
						} // if

						metadata = result.getResultMetadata();
						if (null != metadata) {
							for (int j = 0; j < metadata.length; j++) {
								metadataEntry = metadata[j];
								if (null != metadataEntry) {
									atts.addAttribute("", "name", "name", "", metadataEntry.getName());
									hd.startElement("", "metadata", "dsm:metadata", atts);
									hd.characters(metadataEntry.getValue().toCharArray(), 0, metadataEntry.getValue().length());
									hd.endElement("", "metadata", "dsm:metadata");
									atts.clear();
								} // if
							} // for
						} // if

						// </srw:extraRecordData>
						hd.endElement("", "extraRecordData", "srw:extraRecordData");

						hd.startElement("", "recordPosition", "srw:recordPosition", atts);
						hd.characters(("" + (start + i)).toCharArray(), 0, ("" + (start + i)).length());
						hd.endElement("", "recordPosition", "srw:recordPosition");

						// <srw:record>
						hd.endElement("", "record", "srw:record");
					} // if - non null result
				} // for loop

				// </srw:records>
				hd.endElement("", "records", "srw:records");
			} // if - (null != results && results.length > 0)

			// Next record position
			nextRecordPos = count + start;
			if (nextRecordPos <= total) {
				hd.startElement("", "nextRecordPosition", "srw:nextRecordPosition", atts);
				hd.characters(("" + nextRecordPos).toCharArray(), 0, ("" + nextRecordPos).length());
				hd.endElement("", "nextRecordPosition", "srw:nextRecordPosition");
			}

			// Extra Response Data <srw:extraResponseData>
			atts.clear();
			hd.startElement("", "extraResponseData", "srw:extraResponseData", atts);

			// SOURCES <dsm:sources>

			hd.startElement("", "sources", "dsm:sources", atts);
			SearchAdapterProperties properties;
			for (int i = 0; i < criteria.getSources().length; i++) {
				properties = SearchAdapterFactory.getAdapterProperties(criteria.getSources()[i]);
				if (null != properties) {
					atts.addAttribute("", "id", "id", "", properties.getAdapterCode());
					hd.startElement("", "source", "dsm:source", atts);
					atts.clear();
					hd.startElement("", "title", "dsm:title", atts);
					hd.characters(properties.getDisplayTitle().toCharArray(), 0, properties.getDisplayTitle().length());
					hd.endElement("", "title", "dsm:title");
					hd.startElement("", "description", "dsm:description", atts);
					hd.characters(properties.getDescription().toCharArray(), 0, properties.getDescription().length());
					hd.endElement("", "description", "dsm:description");
					hd.startElement("", "link", "dsm:link", atts);
					hd.characters(properties.getWebsiteURL().toCharArray(), 0, properties.getWebsiteURL().length());
					hd.endElement("", "link", "dsm:link");
					hd.startElement("", "image", "dsm:image", atts);
					hd.characters(properties.getImageURL().toCharArray(), 0, properties.getImageURL().length());
					hd.endElement("", "image", "dsm:image");

					if (operation != SRWRequestHandler.OPERATION_EXPLAIN && null != set) {
						// numresults
						String numResults = "" + set.getResultCountbySource(properties.getAdapterCode());
						hd.startElement("", "numresults", "dsm:numresults", atts);
						hd.characters(numResults.toCharArray(), 0, numResults.length());
						hd.endElement("", "numresults", "dsm:numresults");

						// responsetime
						String responseTime = "" + (set.getTimeTakenBySource(properties.getAdapterCode()) / 1000f);
						atts.addAttribute("", "units", "units", "", "seconds");
						hd.startElement("", "responsetime", "dsm:responsetime", atts);
						atts.clear();
						hd.characters(responseTime.toCharArray(), 0, responseTime.length());
						hd.endElement("", "responsetime", "dsm:responsetime");

						// response code
						String responseCode = set.getResponseCodeOfSource(properties.getAdapterCode());
						String responseDesc = PropertiesService.getStringProperty("responsecode.message." + responseCode, "");
						atts.addAttribute("", "desc", "desc", "", responseDesc);
						hd.startElement("", "responsecode", "dsm:responsecode", atts);
						atts.clear();
						hd.characters(responseCode.toCharArray(), 0, responseCode.length());
						hd.endElement("", "responsecode", "dsm:responsecode");

						// diagnostic
						String diagnostic = set.getDiagnosticOfSource(properties.getAdapterCode());
						hd.startElement("", "diagnostic", "dsm:diagnostic", atts);
						hd.characters(diagnostic.toCharArray(), 0, diagnostic.length());
						hd.endElement("", "diagnostic", "dsm:diagnostic");
					}
					hd.endElement("", "source", "dsm:source");
				}
			}
			hd.endElement("", "sources", "dsm:sources");

			if (operation != SRWRequestHandler.OPERATION_EXPLAIN) {
				// START <dsm:summary>
				hd.startElement("", "summary", "dsm:summary", atts);

				hd.startElement("", "token", "dsm:token", atts);
				hd.characters(("" + token).toCharArray(), 0, ("" + token).length());
				hd.endElement("", "token", "dsm:token");

				hd.startElement("", "count", "dsm:count", atts);
				hd.characters(("" + count).toCharArray(), 0, ("" + count).length());
				hd.endElement("", "count", "dsm:count");

				hd.startElement("", "found", "dsm:found", atts);
				hd.characters(found.toCharArray(), 0, found.length());
				hd.endElement("", "found", "dsm:found");

				hd.startElement("", "theoreticaltotal", "dsm:theoreticaltotal", atts);
				hd.characters(theoreticalTotal.toCharArray(), 0, theoreticalTotal.length());
				hd.endElement("", "theoreticaltotal", "dsm:theoreticaltotal");

				hd.startElement("", "total", "dsm:total", atts);
				hd.characters(("" + total).toCharArray(), 0, ("" + total).length());
				hd.endElement("", "total", "dsm:total");

				atts.addAttribute("", "units", "units", "", "seconds");
				hd.startElement("", "timetaken", "dsm:timetaken", atts);
				atts.clear();
				hd.characters(timeTaken.toCharArray(), 0, timeTaken.length());
				hd.endElement("", "timetaken", "dsm:timetaken");

				hd.startElement("", "completed", "dsm:completed", atts);
				hd.characters(("" + isCompletedSet).toCharArray(), 0, ("" + isCompletedSet).length());
				hd.endElement("", "completed", "dsm:completed");

				// Query Parameters
				atts.addAttribute("", "name", "name", "", "query");
				atts.addAttribute("", "desc", "desc", "", "Query String");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				// if (criteria.getCQLQuery().length() > 0)
				// {
				// hd.characters(criteria.getCQLQuery().toCharArray(),0,criteria.getCQLQuery().length());
				// }
				// else
				// {
				hd.characters(criteria.getQuery().toCharArray(), 0, criteria.getQuery().length());
				// }
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "startRecord");
				atts.addAttribute("", "desc", "desc", "", "Start Record Number");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(("" + start).toCharArray(), 0, ("" + start).length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "maximumRecords");
				atts.addAttribute("", "desc", "desc", "", "Number of Records to be returned");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(("" + pageSize).toCharArray(), 0, ("" + pageSize).length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-mr");
				atts.addAttribute("", "desc", "desc", "", "Total Maximum Results");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(("" + criteria.getMaxResults()).toCharArray(), 0, ("" + criteria.getMaxResults()).length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-cs");
				atts.addAttribute("", "desc", "desc", "", "Case Sensitive");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(("" + criteria.isCaseSensitive()).toCharArray(), 0, ("" + criteria.isCaseSensitive()).length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-kc");
				atts.addAttribute("", "desc", "desc", "", "Keyword Constraint");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(("" + criteria.getKeywordConstraint()).toCharArray(), 0, ("" + criteria.getKeywordConstraint()).length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-rset");
				atts.addAttribute("", "desc", "desc", "", "Repository Filter");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(rset.toCharArray(), 0, rset.length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-ss");
				atts.addAttribute("", "desc", "desc", "", "Search Strategy");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(("" + criteria.getSearchStrategy()).toCharArray(), 0, ("" + criteria.getSearchStrategy()).length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "sortKeys");
				atts.addAttribute("", "desc", "desc", "", "Sort Algorithm");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(sortAlgorithm.toCharArray(), 0, sortAlgorithm.length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-dupes");
				atts.addAttribute("", "desc", "desc", "", "Allow Duplicate Results");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(allowDuplicates.toCharArray(), 0, allowDuplicates.length());
				hd.endElement("", "qp", "dsm:qp");

				atts.addAttribute("", "name", "name", "", "x-user");
				atts.addAttribute("", "desc", "desc", "", "User Code");
				hd.startElement("", "qp", "dsm:qp", atts);
				atts.clear();
				hd.characters(user.toCharArray(), 0, user.length());
				hd.endElement("", "qp", "dsm:qp");

				for (int i = 0; i < criteria.getSources().length; i++) {
					atts.addAttribute("", "name", "name", "", "x-sr");
					atts.addAttribute("", "desc", "desc", "", "Source Resource");
					hd.startElement("", "qp", "dsm:qp", atts);
					atts.clear();
					hd.characters(criteria.getSources()[i].toCharArray(), 0, criteria.getSources()[i].length());
					hd.endElement("", "qp", "dsm:qp");
				}

				for (int i = 0; i < criteria.getThesauri().length; i++) {
					atts.addAttribute("", "name", "name", "", "x-th");
					atts.addAttribute("", "desc", "desc", "", "Thesaurus");
					hd.startElement("", "qp", "dsm:qp", atts);
					atts.clear();
					hd.characters(criteria.getThesauri()[i].toCharArray(), 0, criteria.getThesauri()[i].length());
					hd.endElement("", "qp", "dsm:qp");
				}

				// custom parameters
				if (criteria.hasCustomParams()) {
					for (Enumeration e = criteria.getCustomParams(); e.hasMoreElements();) {
						String customParam = (String) e.nextElement();
						String customParamValue = criteria.getCustomParamValue(customParam);
						atts.addAttribute("", "name", "name", "", "x-" + customParam);
						atts.addAttribute("", "desc", "desc", "", "Adapter Parameter");
						hd.startElement("", "qp", "dsm:qp", atts);
						atts.clear();
						hd.characters(customParamValue.toCharArray(), 0, customParamValue.length());
						hd.endElement("", "qp", "dsm:qp");
					}
				}

				// END </dsm:summary>
				hd.endElement("", "summary", "dsm:summary");

				// Thesauri
				hd.startElement("", "thesaurus", "dsm:thesaurus", atts);

				// Narrower Terms
				if (null != set && set.getNarrowerTerms().length > 0) {
					hd.startElement("", "narrower", "dsm:narrower", atts);
					for (int i = 0; i < set.getNarrowerTerms().length; i++) {
						String word = set.getNarrowerTerms()[i];
						hd.startElement("", "term", "dsm:term", atts);
						hd.characters(word.toCharArray(), 0, word.length());
						hd.endElement("", "term", "dsm:term");
					}
					hd.endElement("", "narrower", "dsm:narrower");
				}

				// Like Terms
				if (null != set && set.getLikeTerms().length > 0) {
					hd.startElement("", "like", "dsm:like", atts);
					for (int i = 0; i < set.getLikeTerms().length; i++) {
						String word = set.getLikeTerms()[i];
						hd.startElement("", "term", "dsm:term", atts);
						hd.characters(word.toCharArray(), 0, word.length());
						hd.endElement("", "term", "dsm:term");
					}
					hd.endElement("", "like", "dsm:like");
				}

				// Broader Terms
				if (null != set && set.getBroaderTerms().length > 0) {
					hd.startElement("", "broader", "dsm:broader", atts);
					for (int i = 0; i < set.getBroaderTerms().length; i++) {
						String word = set.getBroaderTerms()[i];
						hd.startElement("", "term", "dsm:term", atts);
						hd.characters(word.toCharArray(), 0, word.length());
						hd.endElement("", "term", "dsm:term");
					}
					hd.endElement("", "broader", "dsm:broader");
				}

				hd.endElement("", "thesaurus", "dsm:thesaurus");

				// Spelling Suggestion
				hd.startElement("", "spellingsuggestion", "dsm:spellingsuggestion", atts);
				if (null != set) {
					hd.characters(set.getCorrectedTerms().toCharArray(), 0, set.getCorrectedTerms().length());
				}
				hd.endElement("", "spellingsuggestion", "dsm:spellingsuggestion");
			} // operation != explain only

			hd.endElement("", "extraResponseData", "srw:extraResponseData");

			if (operation == SRWRequestHandler.OPERATION_EXPLAIN) {
				// END </srw:searchRetrieveResponse>
				hd.endElement("", "explainResponse", "srw:explainResponse");
			} else {
				// END </srw:searchRetrieveResponse>
				hd.endElement("", "searchRetrieveResponse", "srw:searchRetrieveResponse");
			}

			// End SOAP Envelope
			hd.endElement("", "Body", "soap:Body");
			hd.endElement("", "Envelope", "soap:Envelope");
			hd.endDocument();
		} catch (Exception o) {
			o.printStackTrace();
			showDiagnostics(token, response.getWriter(), "1", o.getMessage(), "General System Error");
		}
		// } else {
		// showDiagnostics(0, response.getWriter(), "3", "", "Authentication
		// Error. Unauthorised user supplied. Check value of x-user
		// parameter.");
		// }

	}

	private void showDiagnostics(long token, PrintWriter out, String diagnosticCode, String details, String message) {
		try {
			message = PropertiesService.getStringProperty("srusrw.diagnostic." + diagnosticCode, message);

			out.println("<SOAP:Envelope xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\">");
			out.println("   <SOAP:Body>");
			out.println("		<srw:diagnostics xmlns:srw=\"http://www.loc.gov/zing/srw/\" xmlns:diag=\"http://www.loc.gov/zing/srw/diagnostic/\">");
			out.println("			<diag:diagnostic>");
			out.println("				<diag:uri>info:srw/diagnostic/1/" + diagnosticCode + "</diag:uri>");
			out.println("				<diag:details>" + details + "</diag:details>");
			out.println("				<diag:message>" + message + "</diag:message>");
			out.println("			</diag:diagnostic>");
			out.println("		</srw:diagnostics>");
			out.println("  </SOAP:Body>");
			out.println("</SOAP:Envelope>");
		} catch (Throwable t) {
			t.printStackTrace();
		}
		if (token > 0) {
			logger.info("RESPONSE " + token + " 0 0 200 " + diagnosticCode);
		}
	}

} // class DSMAPISoap

// -- EOF

